#!/usr/bin/env python
from __future__ import generators
import os, sys
import optparse
import urllib2
import subprocess
import re
import twitter
import simplejson
import common

ANSI_FMT  = "\033[%sm"

ANSI_BLUE = ANSI_FMT % '1;34'
ANSI_BLACK = ANSI_FMT % '1;30'
ANSI_WHITE = ANSI_FMT % '1;37'
ANSI_RESET = "\033[0m"

MAX_POST_LENGTH = 140 # this is the max number of characters allowed by twitter
LINK_REGEX = re.compile('(http[s]?://\S+)')
TINYURL = "http://tinyurl.com/create.php?url=%s"
TINYURL_REGEX = re.compile('name=tinyurl value="(.*)"')

class Clitter(object):
    """
    Clitter is a console twitter client.
    """

    def __init__(self, *args, **kwargs):
        """
        Build a new Clitter instance. Options passed in will override
        a ~/.clitterrc
        """
        # Load up options in the clitter rc file
        self._conf = common.load_conf(kwargs.get('options'))
        # setup twitter API object
        self._api = common.load_api(self._conf)
        # load histories
        self._history = common.load_history(self._conf)

        for channel in ('public','friends'):
            self.fetch(channel)
        # set up colors
        self.colors = self._color_gen()
        self.people_colors = {}

    #
    # Command methods
    #

    def post(self,post):
        "Posts a string to twitter"
        self._api.PostUpdate(self.filter_for_urls(post))

    def test_post_filter(self, post):
        print self.filter_for_urls(post)

    def show(self, search=None):
        try:
            channel = self._conf.channel
        except AttributeError,e:
            channel = 'public'
        tweets = self._history[channel].get(20,search)
        print self._header(channel.upper())
        for t in tweets:
            txt = "%s (%s)" % (self.unescape_entities(t.GetText()), t.GetRelativeCreatedAt())
            try:
                usr = t.GetUser().GetScreenName().encode('utf-8')
                self._display( usr, txt )
            except Exception, e:
                print "Error printing tweet: ", e

    def direct_message(self, msg, at):
        self._api.PostDirectMessage( at, msg )

    def follow(self, user):
        self._api.CreateFriendship(user)
        print "You are now following %s." % user

    def unfollow(self, user):
        self._api.DestroyFriendship(user)
        print "You are no longer following %s." % user

    def show_following(self):
        friends = self._api.GetFriends()
        print self._header("FRIENDS")
        for f in friends:
            status = f.GetStatus()
            if status:
                txt = "%s (%s)" % (self.unescape_entities(status.GetText().encode('utf-8')),
                        status.GetRelativeCreatedAt())
            else:
                txt = "No Status..."
            self._display( f.GetScreenName().encode('utf-8'), txt )

    def show_followers(self):
        followers = self._api.GetFollowers()
        print self._header("FOLLOWERS")
        for f in followers:
            status = f.GetStatus()
            if status:
                txt = "%s (%s)" % (self.unescape_entities(status.GetText().encode('utf-8')),
                        status.GetRelativeCreatedAt())
            else:
                txt = "No Status"
            self._display( f.GetScreenName().encode('utf-8'), txt )

    def daemonize(self):
        # touch the run file
        fp = open( common.RUN_FILE, 'w' )
        # spawn the process
        pid = subprocess.Popen(["python", "daemon.py"]).pid
        fp.write(str(pid))
        fp.close()
        print "Starting daemonized updater... pid: %d" % pid


    def undaemonize(self):
        # kill the pid in the file and remove the file
        fp = open( common.RUN_FILE )
        pid = fp.read()
        fp.close()
        os.unlink( common.RUN_FILE )
        os.kill(int(pid),15) # send sigterm to the process, doesn't work on windows...
        print "Stopping daemonized updater..."

    #
    # Utility Methods
    #

    def fetch(self, channel=None):
        history = self._history.get(channel)
        if history:
            if self._conf.force or history.update():
                history.add(self._fetch(channel))

    def _fetch(self, channel):
        try:
            if channel == "public":
                return self._api.GetPublicTimeline()
            elif channel == "friends":
                return self._api.GetFriendsTimeline(self._conf.username)
        except urllib2.HTTPError, e:
            details = simplejson.loads(e.read())
            print "Sorry, I can't seem to reach twitter right now:" 
            print details.get('error')
        return None

    def unescape_entities( self, string ):
        ents = {"&amp;":"&","&lt;":"<","&gt;":">","&quot;":"\""}
        for k,v in ents.iteritems():
            try:
                string = string.replace(k,v)
            except Exception, e:
                pass
        return string

    def _display(self, usr, txt, width=80):
        next_color = 0
        if usr not in self.people_colors:
            self.people_colors[usr] = self.colors.next()
        p_color = self.people_colors[usr]
        first_line_max = width - (len(usr) + 3)
        txt = self.wrap( txt, first_line_max )
        print "%s<%s%s%s>%s %s" % (ANSI_BLACK, p_color, usr, ANSI_BLACK, ANSI_RESET, txt.encode('utf-8'))

    def wrap( self, s, first_line_max, length=80 ):
        if len(s) > first_line_max:
            marker = first_line_max
            while not s[marker].isspace():
                marker = marker - 1
                if marker <= 0:
                    marker = first_line_max
                    break
            if len(s[marker:]) > length:
                return "\n".join((s[:marker],self.wrap(s[marker:],length).strip()))
            else:
                return "\n".join((s[:marker],s[marker:].strip()))
        else:
            return s

    def filter_for_urls( self, msg ):
        """
        Reads a post looking for http[s]? strings if the msg is too long
        and substitutes the urls with tinyurl replacements.
        """
        if len(msg) > MAX_POST_LENGTH:
            return LINK_REGEX.sub(self.to_tinyurl, msg)
        else:
            return msg

    def to_tinyurl( self, url ):
        """
        Run a url through tiny url and return the shortened url.
        """
        # if this is a match object, just extract the url
        if hasattr(url, 'group'):
            url = url.group()

        return TINYURL_REGEX.search(urllib2.urlopen(TINYURL % url).read()).groups()[0]


    def _header( self, msg, color=None ):
        if not color:
            color = ANSI_BLUE
        return ("%s[%s%s%s]%s" % (ANSI_BLACK,color,msg,ANSI_BLACK,ANSI_RESET)).center(105,"-")

    def _color_gen(self):
        colors = ('0;31','0;32','0;33','0;34','0;35','0;36',
                '1;31','1;32','1;33','1;34','1;35','1;36','1;37')
        cur = 0
        yield ANSI_FMT % colors[cur]
        while 1:
            cur += 1
            if cur >= len(colors):
                cur = 0
            yield ANSI_FMT % colors[cur]


def clitter_command():
    # need to add follow,unfollow as well as listings for followers/followings
    parser = optparse.OptionParser()
    parser.add_option("-c", "--channel", dest="channel",
            help="channel to check, public or friends", metavar="CHANNEL")
    parser.add_option("-p", "--post", dest="post",
            help="post to twitter!", metavar="POST")
    parser.add_option("-f", "--force", dest="force", action="store_true",
            default=False, help="force an update")
    parser.add_option("-s", "--search", dest="search",
            help="search string", metavar="SEARCH", default=None)
    parser.add_option("-d", "--direct-message", dest="dm",
            help="send a direct message, requires you pass in -a also", metavar="DIRECTMESSAGE")
    parser.add_option("-l", "--follow", dest="follow",
            help="start following someone", metavar="FOLLOW")
    parser.add_option("-u", "--unfollow", dest="unfollow",
            help="stop following someone", metavar="UNFOLLOW")
    parser.add_option("-r", "--followers", dest="followers",
            help="get the most recent status of all of your followers",
            action="store_true", default=False)
    parser.add_option("-g", "--following", dest="following",
            help="get the most recent status of everyone you are following",
            action="store_true", default=False)
    parser.add_option("-a", "--at", dest="at",
            help="target a direct message at a user", metavar="AT")
    parser.add_option("-b", "--background", dest="background",
            help="make clitter fetch tweets in the background", action="store_true",
            default=False)
    parser.add_option("-k", "--un-background", dest="unbackground",
            help="stop fetching tweets in the background", action="store_true",
            default=False)
    parser.add_option("-t", "--test-post", dest="test_post",
            help="test the post filter")
    (opts,args) = parser.parse_args()
    app = Clitter(options=opts)

    if opts.dm and opts.at:
        app.direct_message(opts.dm,opts.at)
        app.show(opts.search)
    elif opts.unfollow:
        app.unfollow(opts.unfollow)
    elif opts.follow:
        app.follow(opts.follow)
    elif opts.followers:
        app.show_followers()
    elif opts.following:
        app.show_following()
    elif opts.post:
        app.post(opts.post)
        app.show(opts.search)
    elif opts.test_post:
        app.test_post_filter(opts.test_post)
    elif opts.background:
        app.daemonize()
    elif opts.unbackground:
        app.undaemonize()
    else:
        app.show(opts.search)


if __name__ == "__main__":
    clitter_command()
